package org.example.gateway.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.google.common.collect.Lists;
import org.example.gateway.bean.App;
import org.example.gateway.bean.AppInstance;
import org.example.gateway.bean.RouteRule;
import org.example.gateway.enums.EnableEnum;
import org.example.gateway.enums.GatewayExceptionEnum;
import org.example.gateway.enums.MatchMethodEnum;
import org.example.gateway.enums.MatchObjectEnum;
import org.example.gateway.event.RuleAddEvent;
import org.example.gateway.event.RuleDeleteEvent;
import org.example.gateway.exceptions.GatewayException;
import org.example.gateway.mapper.AppInstanceMapper;
import org.example.gateway.mapper.AppMapper;
import org.example.gateway.mapper.RouteRuleMapper;
import org.example.gateway.pojo.ChangeStatusDto;
import org.example.gateway.pojo.RuleDto;
import org.example.gateway.pojo.dto.AppRuleDto;
import org.example.gateway.pojo.vo.RuleVo;
import org.example.gateway.service.RuleService;
import org.example.gateway.transfer.AppRuleVoTransfer;
import org.example.gateway.transfer.RuleVoTransfer;
import org.example.gateway.utils.DateTimeUtil;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

@Service
public class RuleServiceImpl implements RuleService {
    @Resource
    private RouteRuleMapper ruleMapper;

    @Resource
    private AppMapper appMapper;

    @Resource
    private AppInstanceMapper instanceMapper;

    @Resource
    private ApplicationEventPublisher eventPublisher;

    @Override
    public List<AppRuleDto> getEnabledRule() {
        QueryWrapper<App> wrapper = Wrappers.query();
        wrapper.lambda().eq(App::getEnabled, EnableEnum.ENABLE.getCode());
        List<App> apps = appMapper.selectList(wrapper);
        if (CollectionUtils.isEmpty(apps)) {
            return new ArrayList<>();
        }
        List<Integer> appIds = apps.stream().map(App::getId).collect(Collectors.toList());
        Map<Integer, String> nameMap = apps.stream().collect(Collectors.toMap(App::getId, App::getAppName));
        QueryWrapper<RouteRule> query = Wrappers.query();
        query.lambda().in(RouteRule::getAppId, appIds)
                .eq(RouteRule::getEnabled, EnableEnum.ENABLE.getCode());
        List<RouteRule> routeRules = ruleMapper.selectList(query);
        List<AppRuleDto> appRuleDtos = AppRuleVoTransfer.INSTANCE.mapToVoList(routeRules);
        appRuleDtos.forEach(r -> r.setAppName(nameMap.get(r.getAppId())));
        return appRuleDtos;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void add(RuleDto dto) {
        checkRule(dto);
        App app = appMapper.selectById(dto.getAppId());
        RouteRule routeRule = new RouteRule();
        BeanUtils.copyProperties(dto, routeRule);
        routeRule.setCreatedTime(DateTimeUtil.LocalDateTime2Date(LocalDateTime.now()));
        ruleMapper.insert(routeRule);

        if (EnableEnum.ENABLE.getCode().equals(dto.getEnabled())) {
            AppRuleDto ruleDto = new AppRuleDto();
            BeanUtils.copyProperties(routeRule, ruleDto);
            routeRule.setName(app.getAppName());
            eventPublisher.publishEvent(new RuleAddEvent(this, ruleDto));
        }
    }

    private void checkRule(RuleDto dto) {
        QueryWrapper<RouteRule> wrapper = new QueryWrapper<>();
        wrapper.lambda().eq(RouteRule::getName, dto.getName());
        if (!CollectionUtils.isEmpty(ruleMapper.selectList(wrapper))) {
            throw new GatewayException("规则名称不能重复");
        }
        if (MatchObjectEnum.DEFAULT.getCode().equals(dto.getMatchObject())) {
            dto.setMatchKey(null);
            dto.setMatchMethod(null);
            dto.setMatchRule(null);
        } else {
            if (StringUtils.isEmpty(dto.getMatchKey()) || dto.getMatchMethod() == null
                    || StringUtils.isEmpty(dto.getMatchRule())) {
                throw new GatewayException(GatewayExceptionEnum.PARAM_ERROR);
            }
        }
        // check version
        QueryWrapper<AppInstance> query = Wrappers.query();
        query.lambda().eq(AppInstance::getAppId,dto.getAppId())
                .eq(AppInstance::getVersion,dto.getVersion());
        List<AppInstance> list = instanceMapper.selectList(query);
        if (CollectionUtils.isEmpty(list)){
            throw new GatewayException("实例版本不存在");
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void delete(Integer id) {
        RouteRule routeRule = ruleMapper.selectById(id);
        if (routeRule == null) {
            throw new GatewayException(GatewayExceptionEnum.PARAM_ERROR);
        }
        AppRuleDto dto = new AppRuleDto();
        BeanUtils.copyProperties(routeRule, dto);
        App app = appMapper.selectById(dto.getAppId());
        dto.setAppName(app.getAppName());

        ruleMapper.deleteById(id);
        eventPublisher.publishEvent(new RuleDeleteEvent(this, dto));
    }

    @Override
    public List<RuleVo> queryList(String appName) {
        Integer appId = null;
        if (!StringUtils.isEmpty(appName)) {
            App app = queryByAppName(appName);
            if(app == null) {
                return Lists.newArrayList();
            }
            appId = app.getId();
        }
        QueryWrapper<RouteRule> query = Wrappers.query();
        query.lambda().eq(Objects.nonNull(appId), RouteRule::getAppId, appId).orderByDesc(RouteRule::getCreatedTime);
        List<RouteRule> rules = ruleMapper.selectList(query);
        if (CollectionUtils.isEmpty(rules)) {
            return new ArrayList<>();
        }
        List<RuleVo> ruleVos = RuleVoTransfer.INSTANCE.mapToVOList(rules);
        Map<Integer, String> appMap = getAppNameMap(ruleVos.stream().map(r -> r.getAppId()).collect(Collectors.toList()));
        ruleVos.forEach(ruleVo -> {
            ruleVo.setAppName(appMap.get(ruleVo.getAppId()));
            ruleVo.setMatchStr(buildMatchStr(ruleVo));
        });
        return ruleVos;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void changeStatus(ChangeStatusDto statusDto) {
        RouteRule routeRule = new RouteRule();
        routeRule.setId(statusDto.getId());
        routeRule.setEnabled(statusDto.getEnabled());
        ruleMapper.updateById(routeRule);
        AppRuleDto dto = new AppRuleDto();
        BeanUtils.copyProperties(routeRule, dto);
        dto.setAppName(statusDto.getAppName());
        if (EnableEnum.ENABLE.getCode().equals(statusDto.getEnabled())) {
            eventPublisher.publishEvent(new RuleAddEvent(this, dto));
        } else {
            eventPublisher.publishEvent(new RuleDeleteEvent(this, dto));
        }
    }

    private App queryByAppName(String appName) {
        QueryWrapper<App> wrapper = new QueryWrapper<>();
        wrapper.lambda().eq(App::getAppName, appName);
        App app = appMapper.selectOne(wrapper);
        return app;
    }

    private Map<Integer, String> getAppNameMap(List<Integer> appIdList) {
        QueryWrapper<App> query = Wrappers.query();
        query.lambda().in(App::getId, appIdList);
        List<App> apps = appMapper.selectList(query);
        return apps.stream().collect(Collectors.toMap(App::getId, App::getAppName));
    }

    private String buildMatchStr(RuleVo ruleVo) {
        if (MatchObjectEnum.DEFAULT.getCode().equals(ruleVo.getMatchObject())) {
            return ruleVo.getMatchObject();
        } else {
            StringBuilder sb = new StringBuilder();
            sb.append("[" + ruleVo.getMatchKey() + "]");
            sb.append(MatchMethodEnum.getByCode(ruleVo.getMatchMethod()).getDesc());
            sb.append("[" + ruleVo.getMatchRule() + "]");
            return sb.toString();
        }

    }

}
